using System;
using System.Text;

namespace Com.FastSearch.SharePoint.BL.SearchManager
{
    /// <summary>
    /// Class that can be used to look up user accounts in LDAP.
    /// You may need to use this when integrating with SAM.
    ///
    /// The user ID is then converted to a Fast UID the same way SAM does.
    /// </summary>
    public class UIDHelper
    {

        /// <summary>
        /// Looks up the specified Windows user/group. This works for both
        /// local Windows users/groups and  Active Directory users/groups.
        /// The string returned is the FAST-encoded security ID that should
        /// be placed in the "docacl" field.
        /// This also works towards a remote Active Directory domain.
        /// </summary>
        /// <param name="account">The user/group to look up. Should be on the
        /// form "DOMAIN\username"</param>
        /// <returns>The FAST-encoded security ID or an empty string (if
        /// failure)</returns>
        public static string LookupDomainSecurityAccount(string account)
        {
            return toFBase32(LookupAccoundInLdap(account));
        }

        /// <summary>
        /// Looks up the specified Windows user/group. This works for both
        /// local WIndows users/groups and  Active Directory users/groups.
        /// The string returned is the FAST-encoded security ID that should
        /// be placed in the "docacl" field.
        /// This also works towards a remote Active Directory domain.
        /// </summary>
        /// <param name="domain">The domain where the lookup should be performed.
        /// You need to be allowed to perform LDAP queries towards the domain</param>
        /// <param name="userOrGroupName">The user/group to look up</param>
        /// <returns>The FAST-encoded security ID or an empty string (if
        /// failure)</returns>
        public static string LookupDomainSecurityAccount(string domain, string userOrGroupName)
        {
            return toFBase32(LookupAccoundInLdap(domain, userOrGroupName));
        }

        #region Private stuff

        private static Byte[] LookupAccoundInLdap(string account)
        {
            char[] domainSeparator = new char[2];
            domainSeparator[0] = '\\';
            domainSeparator[1] = '/';
            string[] domainAndUser = account.Split(domainSeparator);

            string domain = domainAndUser[0];
            string user = domainAndUser[1];

            return LookupAccoundInLdap(domain, user);
        }

        private static Byte[] LookupAccoundInLdap(string domain, string userOrGroupName)
        {
            try
            {
                domain = "LDAP://" + domain;

                // Get the user that we authenticate towards Active Directory as (if necessary)
                //string ADUser = ConfigurationSettings.AppSettings["ADUser"].ToString();
                //string ADPass = ConfigurationSettings.AppSettings["ADPassword"].ToString();

                //System.DirectoryServices.DirectoryEntry entry = new System.DirectoryServices.DirectoryEntry(domain, ADUser, ADPass);
                System.DirectoryServices.DirectoryEntry entry = new System.DirectoryServices.DirectoryEntry(domain);
                System.DirectoryServices.DirectorySearcher mySearcher = new System.DirectoryServices.DirectorySearcher(entry);
                mySearcher.Filter = string.Format("(&(|(objectClass=user)(objectClass=group))(sAMAccountName={0}))", userOrGroupName);
                System.DirectoryServices.SearchResult result = mySearcher.FindOne();
                System.DirectoryServices.DirectoryEntry entr = result.GetDirectoryEntry();
                return (Byte[])entr.Properties["objectSid"].Value;
            }
            catch( Exception e )
            {
                throw new Exception( "Unable to contact Active Directory using LDAP", e );
            }
        }

        // Convert binary string to FAST variant of base 32
        // This differs from RFC3548 base 32 in the following ways:
        //    - using lower-case instead of upper-case alphabetic characters
        //    - using digits '1'..'6' instead of '2'..'7'
        //    - no '=' padding at end
        // The first two differences serve no purpose and are just confusing.  However
        // it cannot be changed without breaking existing indexes.
        // The last one is essential (as '=' is not a valid token character in FDS).
        private static string toFBase32(Byte[] source)
        {
            int bytePos = 0;
            int len = source.Length;


            //int dBitPos = 0;
            //unsigned char firstbyte = 0;
            //unsigned char secondbyte = 0;

            int base32len = Base32Length(len);

            // The accumulator contains the two next bytes from the source buffer.
            uint acc;
            int shift = 16 - 5;  // Number of bits to shift current 5-bit group to get its value

            // prime accumulator
            if (len == 0)
            {
                acc = 0;
            }
            else if (len == 1)
            {
                acc = (uint)source[0] << 8;
                bytePos++;
                len--;
            }
            else
            {
                acc = (uint)(source[0] << 8 | source[1]);
                bytePos += 2;
                len -= 2;
            }

            StringBuilder destination = new StringBuilder(base32len + 1);

            for (int i = 0; i < base32len; i++)
            {
                // get current bit group out of accumulator
                char storebyte = (char)((acc >> shift) & 0x1F);
                // unsigned char storebyte = (acc >> shift) & 0x1F;

                // next bit group
                shift -= 5;

                if (shift <= 3)
                {
                    // consumed all of high byte, proceed one byte
                    acc <<= 8;
                    shift += 8;
                    if (len != 0)
                    {
                        // more bytes left
                        acc |= source[bytePos];
                        bytePos++;
                        len--;
                    }
                }

                // add offset 'a' to the byte
                storebyte += 'a';

                if (storebyte > 'z')
                {
                    // out of bounds compared to what we can use from the alphabet, so use numbers for this one
                    storebyte -= (char)('z' + 1);
                    storebyte += '1';

                    // _ASSERT(storebyte < '9');
                }

                // voila! here's our 5 bit string version of this byte
                destination.Append(storebyte);
            }

            return destination.ToString();
        }

        private static int Base32Length(int octets)
        {
            return (octets * 8 + 4) / 5;
        }

        #endregion // Private stuff
    }
}